//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.logging;

import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.Marker;
import org.slf4j.event.Level;
import org.slf4j.event.LoggingEvent;
import org.slf4j.helpers.SubstituteLogger;
import org.slf4j.spi.LocationAwareLogger;

public class JettyLogger implements LocationAwareLogger, Logger
{
    private static final Pattern TRAILING_DIGITS = Pattern.compile("^\\D*(\\d+)$");
    private static final Set<StacklessLogging> stacklessLoggers = new CopyOnWriteArraySet<>();
    private final JettyLoggerFactory factory;
    private final String name;
    private final String condensedName;
    private final JettyAppender appender;
    private JettyLevel level;
    private boolean hideStacks;

    public JettyLogger(JettyLoggerFactory factory, String name, JettyAppender appender)
    {
        this(factory, name, appender, JettyLevel.INFO, false);
    }

    public JettyLogger(JettyLoggerFactory factory, String name, JettyAppender appender, JettyLevel level, boolean hideStacks)
    {
        this.factory = factory;
        this.name = name;
        this.condensedName = condensePackageString(name);
        this.appender = appender;
        this.level = level;
        this.hideStacks = hideStacks;
    }

    /**
     * Condenses a classname by stripping down the package name to just the first character of each package name
     * segment.Configured
     *
     * <pre>
     * Examples:
     * "org.eclipse.jetty.test.FooTest"           = "oejt.FooTest"
     * "org.eclipse.jetty.server.logging.LogTest" = "orjsl.LogTest"
     * </pre>
     *
     * @param classname the fully qualified class name
     * @return the condensed name
     */
    private static String condensePackageString(String classname)
    {
        if (classname == null || classname.isEmpty())
            return "";

        int lastDot = classname.lastIndexOf('.');
        if (lastDot <= 0)
            return classname;

        // This is copied from TypeUtil.toShortName
        StringBuilder b = new StringBuilder();
        String p = classname.substring(0, lastDot);
        String[] ss = p.split("\\.");
        for (String s : ss)
        {
            b.append(s.charAt(0));
            Matcher matcher = TRAILING_DIGITS.matcher(s);
            if (matcher.matches())
                b.append(matcher.group(1));
        }
        b.append('.');
        b.append(classname.substring(lastDot + 1));
        return b.toString();
    }

    public static boolean isStackHidden(JettyLogger logger)
    {
        if (logger == null)
            return false;

        if (logger.isHideStacks())
            return true;

        for (StacklessLogging stacklessLogging : stacklessLoggers)
        {
            if (stacklessLogging.isHiding(logger))
                return true;
        }

        Boolean hidden = JettyLoggerFactory.walkParentLoggerNames(logger.getName(), name ->
        {
            JettyLogger l = logger.factory.getJettyLogger(name, false);
            if (l == null)
                return null;
            if (l.isHideStacks())
                return true;
            for (StacklessLogging stacklessLogging : stacklessLoggers)
            {
                if (stacklessLogging.isHiding(l))
                    return true;
            }
            return null;
        });

        return Boolean.TRUE.equals(hidden);
    }

    public JettyAppender getAppender()
    {
        return appender;
    }

    String getCondensedName()
    {
        return condensedName;
    }

    public JettyLevel getLevel()
    {
        return level;
    }

    public void setLevel(JettyLevel level)
    {
        this.level = level;

        // apply setLevel to children too.
        factory.walkChildrenLoggers(this.getName(), (logger) -> logger.setLevel(level));
    }

    @Override
    public String getName()
    {
        return name;
    }

    public boolean isHideStacks()
    {
        return hideStacks;
    }

    public void setHideStacks(boolean hideStacks)
    {
        this.hideStacks = hideStacks;
    }

    static void add(StacklessLogging stacklessLogging)
    {
        stacklessLoggers.add(stacklessLogging);
    }

    static void remove(StacklessLogging stacklessLogging)
    {
        stacklessLoggers.remove(stacklessLogging);
    }

    @Override
    public void debug(String msg)
    {
        if (isDebugEnabled())
        {
            emit(Level.DEBUG, msg);
        }
    }

    @Override
    public void debug(String format, Object arg)
    {
        if (isDebugEnabled())
        {
            emit(Level.DEBUG, format, arg);
        }
    }

    @Override
    public void debug(String format, Object arg1, Object arg2)
    {
        if (isDebugEnabled())
        {
            emit(Level.DEBUG, format, arg1, arg2);
        }
    }

    @Override
    public void debug(String format, Object... arguments)
    {
        if (isDebugEnabled())
        {
            emit(Level.DEBUG, format, arguments);
        }
    }

    @Override
    public void debug(String msg, Throwable throwable)
    {
        if (isDebugEnabled())
        {
            emit(Level.DEBUG, msg, throwable);
        }
    }

    @Override
    public void debug(Marker marker, String msg)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        debug(msg);
    }

    @Override
    public void debug(Marker marker, String format, Object arg)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        debug(format, arg);
    }

    @Override
    public void debug(Marker marker, String format, Object arg1, Object arg2)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        debug(format, arg1, arg2);
    }

    @Override
    public void debug(Marker marker, String format, Object... arguments)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        debug(format, arguments);
    }

    @Override
    public void debug(Marker marker, String msg, Throwable t)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        debug(msg, t);
    }

    @Override
    public boolean isDebugEnabled()
    {
        return level.includes(JettyLevel.DEBUG);
    }

    @Override
    public boolean isDebugEnabled(Marker marker)
    {
        return isDebugEnabled();
    }

    @Override
    public void error(String msg)
    {
        if (isErrorEnabled())
        {
            emit(Level.ERROR, msg);
        }
    }

    @Override
    public void error(String format, Object arg)
    {
        if (isErrorEnabled())
        {
            emit(Level.ERROR, format, arg);
        }
    }

    @Override
    public void error(String format, Object arg1, Object arg2)
    {
        if (isErrorEnabled())
        {
            emit(Level.ERROR, format, arg1, arg2);
        }
    }

    @Override
    public void error(String format, Object... arguments)
    {
        if (isErrorEnabled())
        {
            emit(Level.ERROR, format, arguments);
        }
    }

    @Override
    public void error(String msg, Throwable throwable)
    {
        if (isErrorEnabled())
        {
            emit(Level.ERROR, msg, throwable);
        }
    }

    @Override
    public void error(Marker marker, String msg)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        error(msg);
    }

    @Override
    public void error(Marker marker, String format, Object arg)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        error(format, arg);
    }

    @Override
    public void error(Marker marker, String format, Object arg1, Object arg2)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        error(format, arg1, arg2);
    }

    @Override
    public void error(Marker marker, String format, Object... arguments)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        error(format, arguments);
    }

    @Override
    public void error(Marker marker, String msg, Throwable t)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        error(msg, t);
    }

    @Override
    public boolean isErrorEnabled()
    {
        return level.includes(JettyLevel.ERROR);
    }

    @Override
    public boolean isErrorEnabled(Marker marker)
    {
        return isErrorEnabled();
    }

    @Override
    public void info(String msg)
    {
        if (isInfoEnabled())
        {
            emit(Level.INFO, msg);
        }
    }

    @Override
    public void info(String format, Object arg)
    {
        if (isInfoEnabled())
        {
            emit(Level.INFO, format, arg);
        }
    }

    @Override
    public void info(String format, Object arg1, Object arg2)
    {
        if (isInfoEnabled())
        {
            emit(Level.INFO, format, arg1, arg2);
        }
    }

    @Override
    public void info(String format, Object... arguments)
    {
        if (isInfoEnabled())
        {
            emit(Level.INFO, format, arguments);
        }
    }

    @Override
    public void info(String msg, Throwable throwable)
    {
        if (isInfoEnabled())
        {
            emit(Level.INFO, msg, throwable);
        }
    }

    @Override
    public void info(Marker marker, String msg)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        info(msg);
    }

    @Override
    public void info(Marker marker, String format, Object arg)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        info(format, arg);
    }

    @Override
    public void info(Marker marker, String format, Object arg1, Object arg2)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        info(format, arg1, arg2);
    }

    @Override
    public void info(Marker marker, String format, Object... arguments)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        info(format, arguments);
    }

    @Override
    public void info(Marker marker, String msg, Throwable t)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        info(msg, t);
    }

    @Override
    public boolean isInfoEnabled()
    {
        return level.includes(JettyLevel.INFO);
    }

    @Override
    public boolean isInfoEnabled(Marker marker)
    {
        return isInfoEnabled();
    }

    @Override
    public void trace(String msg)
    {
        if (isTraceEnabled())
        {
            emit(Level.TRACE, msg);
        }
    }

    @Override
    public void trace(String format, Object arg)
    {
        if (isTraceEnabled())
        {
            emit(Level.TRACE, format, arg);
        }
    }

    @Override
    public void trace(String format, Object arg1, Object arg2)
    {
        if (isTraceEnabled())
        {
            emit(Level.TRACE, format, arg1, arg2);
        }
    }

    @Override
    public void trace(String format, Object... arguments)
    {
        if (isTraceEnabled())
        {
            emit(Level.TRACE, format, arguments);
        }
    }

    @Override
    public void trace(String msg, Throwable throwable)
    {
        if (isTraceEnabled())
        {
            emit(Level.TRACE, msg, throwable);
        }
    }

    @Override
    public void trace(Marker marker, String msg)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        trace(msg);
    }

    @Override
    public void trace(Marker marker, String format, Object arg)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        trace(format, arg);
    }

    @Override
    public void trace(Marker marker, String format, Object arg1, Object arg2)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        trace(format, arg1, arg2);
    }

    @Override
    public void trace(Marker marker, String format, Object... argArray)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        trace(format, argArray);
    }

    @Override
    public void trace(Marker marker, String msg, Throwable t)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        trace(msg, t);
    }

    @Override
    public boolean isTraceEnabled()
    {
        return level.includes(JettyLevel.TRACE);
    }

    @Override
    public boolean isTraceEnabled(Marker marker)
    {
        return isTraceEnabled();
    }

    @Override
    public void warn(String msg)
    {
        if (isWarnEnabled())
        {
            emit(Level.WARN, msg);
        }
    }

    @Override
    public void warn(String format, Object arg)
    {
        if (isWarnEnabled())
        {
            emit(Level.WARN, format, arg);
        }
    }

    @Override
    public void warn(String format, Object... arguments)
    {
        if (isWarnEnabled())
        {
            emit(Level.WARN, format, arguments);
        }
    }

    @Override
    public void warn(String format, Object arg1, Object arg2)
    {
        if (isWarnEnabled())
        {
            emit(Level.WARN, format, arg1, arg2);
        }
    }

    @Override
    public void warn(String msg, Throwable throwable)
    {
        if (isWarnEnabled())
        {
            emit(Level.WARN, msg, throwable);
        }
    }

    @Override
    public void warn(Marker marker, String msg)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        warn(msg);
    }

    @Override
    public void warn(Marker marker, String format, Object arg)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        warn(format, arg);
    }

    @Override
    public void warn(Marker marker, String format, Object arg1, Object arg2)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        warn(format, arg1, arg2);
    }

    @Override
    public void warn(Marker marker, String format, Object... arguments)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        warn(format, arguments);
    }

    @Override
    public void warn(Marker marker, String msg, Throwable t)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        warn(msg, t);
    }

    @Override
    public boolean isWarnEnabled()
    {
        return level.includes(JettyLevel.WARN);
    }

    @Override
    public boolean isWarnEnabled(Marker marker)
    {
        return isWarnEnabled();
    }

    private void emit(Level level, String msg)
    {
        long timestamp = System.currentTimeMillis();
        String threadName = Thread.currentThread().getName();
        getAppender().emit(this, level, timestamp, threadName, null, msg);
    }

    private void emit(Level level, String format, Object arg)
    {
        long timestamp = System.currentTimeMillis();
        String threadName = Thread.currentThread().getName();
        if (arg instanceof Throwable)
            getAppender().emit(this, level, timestamp, threadName, (Throwable)arg, format);
        else
            getAppender().emit(this, level, timestamp, threadName, null, format, arg);
    }

    private void emit(Level level, String format, Object arg1, Object arg2)
    {
        long timestamp = System.currentTimeMillis();
        String threadName = Thread.currentThread().getName();
        if (arg2 instanceof Throwable)
            getAppender().emit(this, level, timestamp, threadName, (Throwable)arg2, format, arg1);
        else
            getAppender().emit(this, level, timestamp, threadName, null, format, arg1, arg2);
    }

    private void emit(Level level, String format, Object... arguments)
    {
        long timestamp = System.currentTimeMillis();
        String threadName = Thread.currentThread().getName();
        getAppender().emit(this, level, timestamp, threadName, null, format, arguments);
    }

    private void emit(Level level, String msg, Throwable throwable)
    {
        long timestamp = System.currentTimeMillis();
        String threadName = Thread.currentThread().getName();
        getAppender().emit(this, level, timestamp, threadName, throwable, msg);
    }

    /**
     * Entry point for {@link LocationAwareLogger}
     */
    @Override
    public void log(Marker marker, String fqcn, int levelInt, String message, Object[] argArray, Throwable throwable)
    {
        if (this.level.toInt() <= levelInt)
        {
            long timestamp = System.currentTimeMillis();
            String threadName = Thread.currentThread().getName();
            getAppender().emit(this, JettyLevel.intToLevel(levelInt).toLevel(), timestamp, threadName, throwable, message, argArray);
        }
    }

    /**
     * Dynamic (via Reflection) entry point for {@link SubstituteLogger} usage.
     *
     * @param event the logging event
     */
    @SuppressWarnings("unused")
    public void log(LoggingEvent event)
    {
        // TODO: do we want to support org.sfl4j.Marker?
        // TODO: do we want to support org.sfl4j.even.KeyValuePair?
        getAppender().emit(this, event.getLevel(), event.getTimeStamp(), event.getThreadName(), event.getThrowable(), event.getMessage(), event.getArgumentArray());
    }

    @Override
    public String toString()
    {
        return String.format("%s:%s:LEVEL=%s", JettyLogger.class.getSimpleName(), name, level.name());
    }
}
